Skip to content

feat: implement BitGo signing in SDK#8624

Open
alextse-bg wants to merge 3 commits intomasterfrom
WCN-217
Open

feat: implement BitGo signing in SDK#8624
alextse-bg wants to merge 3 commits intomasterfrom
WCN-217

Conversation

@alextse-bg
Copy link
Copy Markdown
Contributor

@alextse-bg alextse-bg commented Apr 23, 2026

Commit 1: allowing trading wallet transaction signing on Trading Account Objects

make wallet passphrase optional when signing OFC transactions.
if not present, the SDK attempts to sign using the wallet's BitGo key instead.

Commit 2: allowing trading wallet transaction signing on Wallet and Coins object

The following are all of the currently valid methods to create a signature on an OFC wallet's payload string

  1. ofcToken.signMessage({prv}, message): encrypts the message locally using prv
  2. ofcToken.signTransaction(params): signs a half signed transaction by calling the above method
  3. wallet.baseCoin.signMessage: see above
  4. wallet.baseCoin.signTransaction: see above
  5. wallet.signTransaction(params): signs a half signed transaction by getting the prv through a wallet passphrase then calling this.baseCoin.signTransaction
  6. wallet.prebuildAndSignTransaction(params): builds and sign a transaction by calling wallet.signTransaction
  7. all other wallet methods that calls wallet.prebuildAndSignTransaction (e.g. sendMany)
  8. wallet.toTradingAccount().signPayload: signs a half signed transaction using the wallet passphrase

Changes in commit 1 address path 8 already.

For paths that creates the signature using methods of wallet object (i.e. 5-7), all of them eventually calls wallet.signTransaction, which pass itself this as an argument to wallet.baseCoin.signTransaction (see here), allowing us to sign via BitGo key if we add the implementation to ofcToken

As for ofcToken.signMessage, add overloads to the method to allow SDK user to pass in the wallet object instead, which creates the signature via the BitGo key.

Note that the walletPassphrase is already an optional parameter when calling wallet level methods.

@linear
Copy link
Copy Markdown

linear Bot commented Apr 23, 2026

allow wallet and coins object to sign using the BitGo key
if the passphrase is not provided during signing

Ticket: WCN-217-2
When no walletPassphrase is present in the request body or environment,
pass undefined to tradingAccount.signPayload() instead of throwing.
The SDK routes passphrase-less signing through KMS internally.

Ticket: WCN-215-1
@alextse-bg alextse-bg marked this pull request as ready for review April 24, 2026 17:39
@alextse-bg alextse-bg requested review from a team as code owners April 24, 2026 17:39
export interface SignPayloadParameters {
payload: string | Record<string, unknown>;
walletPassphrase: string;
walletPassphrase?: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: add jsdoc describing why this is optional.

Comment on lines +34 to +36
if (!params.walletPassphrase) {
return this.signPayloadByBitGoKey(params);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First check the wallet setting.

If there is no walletPassphrase and wallet.userKeySigningRequired is set to true, then throw an error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backend will check it anyways, but best to do the validation here as well.

}

/**
* Signs the payload of a trading account via the trading account BitGo key stored in a remote KMS
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Signs the payload of a trading account via the BitGo key

private async signPayloadByBitGoKey(params: Omit<SignPayloadParameters, 'walletPassphrase'>): Promise<string> {
const walletData = this.wallet.toJSON();
if (walletData.userKeySigningRequired) {
throw new Error('Wallet must use user key to sign ofc transaction, please provide the wallet passphrase');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw new Error('Wallet must use user key to sign ofc transaction, please provide the wallet passphrase or visit your wallet settings page to configure one.');

Comment on lines +51 to +53
if (walletData.keys.length < 2) {
throw new Error('Wallet does not support BitGo signing');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw new Error('Wallet does not support BitGo signing. Please reach out to support@bitgo.com to resolve this issue.');

async signMessage(key: { prv: string }, message: string): Promise<Buffer>;
async signMessage(keyOrWallet: { prv: string } | Wallet, message: string): Promise<Buffer> {
if (!(keyOrWallet instanceof Wallet)) {
return super.signMessage(keyOrWallet as { prv: string }, message);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you still need the cast here? I thought the type would be narrowed by the time you are inside this if block.

if (!(keyOrWallet instanceof Wallet)) {
return super.signMessage(keyOrWallet as { prv: string }, message);
}
const signatureHexString = await (keyOrWallet as Wallet).toTradingAccount().signPayload({ payload: message });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, I'm surprised you still need a cast after type narrowing.


/**
* Returns the wallet passphrase from the environment, or undefined if not set.
* Unlike getWalletPwFromEnv, this does not throw when the env variable is absent.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just update getWalletPwFromEnv to make the throwing behaviour configurable?

Copy link
Copy Markdown
Contributor

@zahin-mohammad zahin-mohammad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, just a few suggestions/comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants